// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) Huawei Technologies Co., Ltd. 2023. All rights reserved.
 */

#include <linux/memory_patrol.h>
#include <linux/mm.h>
#include <linux/page-isolation.h>
#include "internal.h"

static inline bool pages_check_hwpoison(struct page *page, unsigned long nr_pages)
{
	unsigned long i = 0;

	for (i = 0; i < nr_pages; i++) {
		if (PageHWPoison(page))
			return true;

		page = nth_page(page, 1);
	}

	return false;
}

static inline void return_unhwpoison_pages(struct page *page, struct zone *zone,
						unsigned long nr_pages)
{
	unsigned long i;

	for (i = 0; i < nr_pages; i++) {
		if (!PageHWPoison(page))
			return_page_to_buddy(page, zone);

		page = nth_page(page, 1);
	}
}

static int unisolate_buddy_pages(unsigned long pfn, unsigned long nr_pages)
{
	struct page *page = pfn_to_page(pfn);
	struct zone *zone;
	unsigned long nr_pageblock = nr_pages / pageblock_nr_pages;
	unsigned long i, flags;
	bool hwpoison_pageblock;

	for (i = 0; i < nr_pageblock; i++) {
		zone = page_zone(page);
		hwpoison_pageblock = pages_check_hwpoison(page, pageblock_nr_pages);

		spin_lock_irqsave(&zone->lock, flags);
		if (!hwpoison_pageblock)
			return_pageblock_to_buddy(page, zone);
		else
			return_unhwpoison_pages(page, zone, pageblock_nr_pages);
		spin_unlock_irqrestore(&zone->lock, flags);

		page = nth_page(page, pageblock_nr_pages);
	}

	return 0;
}

static bool pageblock_in_freelist(unsigned long pfn, struct zone *zone,
			struct page **out)
{
	struct page *start_page;
	unsigned long start_pfn;
	int order;

	if (out)
		*out = NULL;

	for (order = pageblock_order; order < MAX_ORDER; order++) {
		start_pfn = pfn & ~((1 << order) - 1);
		if (!pfn_valid(start_pfn))
			continue;

		start_page = pfn_to_page(start_pfn);

		if (zone != page_zone(start_page))
			break;

		if (PageBuddy(start_page) && buddy_order(start_page) == order) {
			if (out)
				*out = start_page;
			return true;
		}
	}

	return false;
}

static int check_pageblocks_in_freelist(unsigned long start_pfn, unsigned long nr_pageblock)
{
	unsigned long pfn = start_pfn;
	struct zone *zone;
	struct page *page;
	unsigned long i;

	for (i = 0; i < nr_pageblock; i++) {
		page = pfn_to_page(pfn);
		zone = page_zone(page);

		if (!pageblock_in_freelist(pfn, zone, NULL))
			return -EBUSY;

		if (pages_check_hwpoison(page, pageblock_nr_pages))
			return -EHWPOISON;

		pfn += pageblock_nr_pages;
		if (!pfn_valid(pfn))
			return -EINVAL;
	}

	return 0;
}

static void __isolate_buddy_pages(unsigned long start_pfn, unsigned long nr_pageblock,
				struct zone *zone)
{
	unsigned long pfn;
	unsigned long i;

	for (i = 0, pfn = start_pfn; i < nr_pageblock; i++) {
		struct page *start_page;
		struct page *pageblock_page = pfn_to_page(pfn);

		pageblock_in_freelist(pfn, zone, &start_page);

		take_pageblock_off_buddy(start_page, pageblock_page);

		pfn += pageblock_nr_pages;
	}
}

static bool check_watermark_ok(unsigned long start_pfn, unsigned long nr_pages)
{
	struct zone *zone;
	unsigned long pfn = start_pfn;
	unsigned long zone_nr_pages;

	while (nr_pages > 0) {
		zone = page_zone(pfn_to_page(pfn));
		if (zone_end_pfn(zone) >= (pfn + nr_pages))
			zone_nr_pages = nr_pages;
		else
			zone_nr_pages = zone_end_pfn(zone) - pfn;

		if (zone_page_state(zone, NR_FREE_PAGES) - zone_nr_pages < low_wmark_pages(zone))
			return false;

		nr_pages -= zone_nr_pages;
		pfn += zone_nr_pages;
	}

	return true;
}

/*
 * Isolate pageblocks between [start_pfn, start_pfn + nr_pages).
 * the range may be cross differrent zones.
 */
static int isolate_buddy_pages(unsigned long start_pfn, unsigned long nr_pages)
{
	struct zone *zone;
	unsigned long pfn = start_pfn;
	unsigned long left_nr_pages = nr_pages;
	unsigned long zone_nr_pages, zone_nr_pageblocks;
	unsigned long flags;
	int ret = 0;

	if (!check_watermark_ok(start_pfn, nr_pages))
		return -ENOMEM;

	while (left_nr_pages > 0) {
		zone = page_zone(pfn_to_page(pfn));
		if (zone_end_pfn(zone) >= (pfn + left_nr_pages))
			zone_nr_pages = left_nr_pages;
		else
			zone_nr_pages = zone_end_pfn(zone) - pfn;

		if (!IS_ALIGNED(zone_nr_pages, pageblock_nr_pages)) {
			ret = -EINVAL;
			break;
		}

		zone_nr_pageblocks = zone_nr_pages / pageblock_nr_pages;

		drain_all_pages(zone);
		spin_lock_irqsave(&zone->lock, flags);
		ret = check_pageblocks_in_freelist(pfn, zone_nr_pageblocks);
		if (ret) {
			spin_unlock_irqrestore(&zone->lock, flags);
			break;
		}

		__isolate_buddy_pages(pfn, zone_nr_pageblocks, zone);
		spin_unlock_irqrestore(&zone->lock, flags);

		left_nr_pages -= zone_nr_pages;
		pfn += zone_nr_pages;
	}

	if (left_nr_pages > 0)
		unisolate_buddy_pages(start_pfn, nr_pages - left_nr_pages);

	return ret;
}

static void get_free_buddy_from_zone(struct seq_file *m, struct zone *zone)
{
	unsigned long flags;
	int order;
	unsigned long region;

	spin_lock_irqsave(&zone->lock, flags);
	for (order = pageblock_order; order < MAX_ORDER; order++) {
		struct free_area *free_area = &(zone->free_area[order]);
		int migratetype;

		for (migratetype = 0; migratetype < MIGRATE_TYPES; migratetype++) {
			struct list_head *list = &free_area->free_list[migratetype];
			struct page *page;

			if (is_migrate_isolate(migratetype))
				continue;

			list_for_each_entry(page, list, lru) {
				if (pages_check_hwpoison(page, 1 << order))
					continue;

				region = encode_free_range(page_to_pfn(page), order);
				seq_write(m, &region, sizeof(region));
			}
		}
	}
	spin_unlock_irqrestore(&zone->lock, flags);
}

static void get_free_buddy(struct seq_file *m, int nid)
{
	struct pglist_data *pgdat = NODE_DATA(nid);
	struct zone *zone;
	int zoneid;

	for (zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
		zone = &pgdat->node_zones[zoneid];
		if (!populated_zone(zone))
			continue;

		drain_all_pages(zone);
		get_free_buddy_from_zone(m, zone);
	}
}

/*
 *  A page mananged by buddy may have be allocated and
 *  cann't be checked with PageBuddy.
 */
static bool is_page_managed_by_buddy(unsigned long pfn)
{
	return pfn_valid(pfn) && !PageHuge(pfn_to_page(pfn));
}

static struct scan_mem_operations scan_mem_buddy_ops = {
	.name = "buddy",
	.sprint_free_pages = get_free_buddy,
	.is_page_managed = is_page_managed_by_buddy,
	.alloc_pages_pfn = isolate_buddy_pages,
	.free_pages_pfn = unisolate_buddy_pages,
};

static int __init register_mem_patrol_for_buddy(void)
{
	return register_mem_patrol(&scan_mem_buddy_ops);
}
late_initcall(register_mem_patrol_for_buddy);
